package pl.s7n.paint;

import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Event;
import java.awt.EventQueue;
import java.awt.FlowLayout;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.text.ParseException;

import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JColorChooser;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JSpinner;
import javax.swing.JToggleButton;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;
import javax.swing.SpinnerNumberModel;
import javax.swing.UIManager;
import javax.swing.border.BevelBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import pl.s7n.paint.commands.MirrorHTransformCmd;
import pl.s7n.paint.commands.MirrorVTransformCmd;
import pl.s7n.paint.commands.ResizeCmd;
import pl.s7n.paint.commands.Rot180TransformCmd;
import pl.s7n.paint.commands.RotLeftTransformCmd;
import pl.s7n.paint.commands.RotRightTransformCmd;
import pl.s7n.paint.utils.JButtonAsColor;
import pl.s7n.paint.utils.SelfUpdatable;
import pl.s7n.paint.utils.Utils;

/**
 * Main class containing main() method and starting up the whole application.<br>
 * Consists of the main window frame, the drawing canvas & undo manager.<br>
 * 
 * @author 7 (rychlik.pawel@gmail.com)
 */
public class AppWindow implements SelfUpdatable {

  private JFrame            frame;

  // undo support manager
  private CanvasUndoManager manager;

  // beware. here comes the canvas.
  private DrawableCanvas    canvas;

  // buttons & menu items whose state depend on undo manager
  private JMenuItem         btnUndo;
  private JMenuItem         btnRedo;
  private JButton           btnUndoToolbar;
  private JButton           btnRedoToolbar;

  // last save-to location (for direct Save)
  private File              saveFile;
  
  // coors displayed here
  private JLabel            statusLabel;

  /**
   * Launch the application.
   */
  public static void main(String[] args) {
    EventQueue.invokeLater(new Runnable() {
      public void run() {
        try {
          try {
            UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
          } catch (Exception e) { // don't worry.
          }

          AppWindow window = new AppWindow();
          window.frame.setVisible(true);
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
    });
  }

  /**
   * Create the application.
   */
  public AppWindow() {
    initializeGui();

    bindEditManager();

    bindCoordsStatusBar();
  }

  /**
   * binds button actions and keystrokes to undo manager.
   */
  private void bindEditManager() {
    // undo manager:
    manager = new CanvasUndoManager(this);
    canvas.addUndoableEditListener(manager);

    btnUndo.addActionListener(manager.defaultUndoAction());
    btnUndoToolbar.addActionListener(manager.defaultUndoAction());

    btnRedo.addActionListener(manager.defaultRedoAction());
    btnRedoToolbar.addActionListener(manager.defaultRedoAction());

    ActionMap actionMap = frame.getRootPane().getActionMap();
    actionMap.put("UndoAction", manager.defaultUndoAction());
    actionMap.put("RedoAction", manager.defaultRedoAction());

    InputMap inputMap = frame.getRootPane().getInputMap();
    inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_Z, Event.CTRL_MASK), "UndoAction");
    inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_Y, Event.CTRL_MASK), "RedoAction");

  }

  /**
   * binds mouse moves over canvas with displaying canvas coords @ status bar
   */
  private void bindCoordsStatusBar() {
    MouseAdapter coordsMouseAdapter = new MouseAdapter() {
      @Override
      public void mouseMoved(MouseEvent e) {
        super.mouseMoved(e);
        statusLabel.setText(canvas.getReadableCoords(e.getPoint()));
      }

      @Override
      public void mouseExited(MouseEvent e) {
        statusLabel.setText(" ");
        super.mouseExited(e);
      }
    };
    canvas.addMouseMotionListener(coordsMouseAdapter);
    canvas.addMouseListener(coordsMouseAdapter);
  }

  /**
   * Initialize the contents of the frame.
   */
  private void initializeGui() {
    frame = new JFrame();
    frame.setBounds(100, 100, 850, 500);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.setTitle("FPPaint2");

    buildMenu();

    buildStatusBar();

    buildToolbar();

    btnUndo.setEnabled(false);
    btnRedo.setEnabled(false);
    btnUndoToolbar.setEnabled(false);
    btnRedoToolbar.setEnabled(false);

    canvas = new DrawableCanvas(640, 480);
    JScrollPane scrollPane = new JScrollPane(canvas);

    frame.getContentPane().add(scrollPane, BorderLayout.CENTER);
  }

  /**
   * THeres no JStatusBar in Swing, so build up my own.
   */
  private void buildStatusBar() {
    JPanel statusPanel = new JPanel();
    statusPanel.setLayout(new BorderLayout());
    statusPanel.add(statusLabel = new JLabel(" "), BorderLayout.EAST);
    statusPanel.setBorder(new BevelBorder(BevelBorder.LOWERED));

    frame.getContentPane().add(statusPanel, BorderLayout.SOUTH);
  }

  /**
   * Set up the menu items & bind their actions properly.
   */
  private void buildMenu() {
    JMenuBar menuBar = new JMenuBar();
    frame.setJMenuBar(menuBar);

    ///////////////// File //////////////////

    JMenu mnFile = new JMenu("File");
    menuBar.add(mnFile);

    JMenuItem mntmNew = new JMenuItem("New");
    mntmNew.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent arg0) {
        newImage();
      }
    });
    mnFile.add(mntmNew);

    JMenuItem mntmOpen = new JMenuItem("Open");
    mntmOpen.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent arg0) {
        open();
      }
    });
    mnFile.add(mntmOpen);

    JMenuItem mntmSaveAs = new JMenuItem("Save As ...");
    mntmSaveAs.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent arg0) {
        saveAs();
      }
    });
    mnFile.add(mntmSaveAs);

    JMenuItem mntmSave = new JMenuItem("Save");
    mntmSave.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent arg0) {
        save();
      }
    });
    mnFile.add(mntmSave);

    JSeparator separator_1 = new JSeparator();
    mnFile.add(separator_1);

    JMenuItem mntmExit = new JMenuItem("Exit");
    mntmExit.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        exit();
      }
    });
    mnFile.add(mntmExit);

    ///////////////// Edit //////////////////
    JMenu mnEdit = new JMenu("Edit");
    menuBar.add(mnEdit);

    btnUndo = new JMenuItem("Undo");
    mnEdit.add(btnUndo);

    btnRedo = new JMenuItem("Redo");
    mnEdit.add(btnRedo);

    /////////////////// Transform //////////////////

    JMenu mnTransform = new JMenu("Transform");
    menuBar.add(mnTransform);

    JMenuItem mntmRotateLeft = new JMenuItem("Rotate Left");
    mntmRotateLeft.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        canvas.getResizeEditManager().editPerformed(new RotLeftTransformCmd(canvas.getImage()));
      }
    });

    JMenuItem mntmResizeImage = new JMenuItem("Resize Image");
    mntmResizeImage.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent arg0) {
        resize();
      }
    });
    mnTransform.add(mntmResizeImage);

    JSeparator separator_2 = new JSeparator();
    mnTransform.add(separator_2);
    mnTransform.add(mntmRotateLeft);

    JMenuItem mntmRotateRight = new JMenuItem("Rotate Right");
    mntmRotateRight.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        canvas.getResizeEditManager().editPerformed(new RotRightTransformCmd(canvas.getImage()));
      }
    });
    mnTransform.add(mntmRotateRight);

    JMenuItem mntmRotate180 = new JMenuItem("Rotate 180");
    mntmRotate180.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        canvas.getResizeEditManager().editPerformed(new Rot180TransformCmd(canvas.getImage()));
      }
    });
    mnTransform.add(mntmRotate180);

    JMenuItem mntmMirrorH = new JMenuItem("Mirror H");
    mntmMirrorH.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        canvas.getResizeEditManager().editPerformed(new MirrorHTransformCmd(canvas.getImage()));
      }
    });

    JSeparator separator = new JSeparator();
    mnTransform.add(separator);
    mnTransform.add(mntmMirrorH);

    JMenuItem mntmMirrorV = new JMenuItem("Mirror V");
    mntmMirrorV.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        canvas.getResizeEditManager().editPerformed(new MirrorVTransformCmd(canvas.getImage()));
      }
    });
    mnTransform.add(mntmMirrorV);
  }

  /**
   * Create the drawing tools bar, set up the buttons, color and brush size pickers, and bind their actions.
   */
  private void buildToolbar() {
    final Dimension paintingToolBtnSize = new Dimension(76, 25);
    final Dimension undoRedoBtnSize = new Dimension(36, 25);

    JToolBar toolbar = new JToolBar("Toolbar", JToolBar.VERTICAL);
    toolbar.setBorder(BorderFactory.createLineBorder(Color.BLACK));

    //    Utils.setFixedSize(toolbar, new Dimension(100, 1280));
    toolbar.setMaximumSize(new Dimension(50, 1280));
    frame.getContentPane().add(toolbar, BorderLayout.WEST);

    JPanel undoRedoPanel = new JPanel();
    FlowLayout flowLayout = (FlowLayout) undoRedoPanel.getLayout();
    flowLayout.setVgap(2);
    flowLayout.setHgap(2);
    Dimension dim = new Dimension(80, 28);
    undoRedoPanel.setMaximumSize(dim);
    undoRedoPanel.setMinimumSize(dim);
    undoRedoPanel.setPreferredSize(dim);
    toolbar.add(undoRedoPanel);

    btnUndoToolbar = new JButton(Utils.loadImageIcon("icons/Undo-icon.png"));
    Utils.setFixedSize(btnUndoToolbar, undoRedoBtnSize);
    btnUndoToolbar.setToolTipText("Undo");
    undoRedoPanel.add(btnUndoToolbar);

    btnRedoToolbar = new JButton(Utils.loadImageIcon("icons/Redo-icon.png"));
    Utils.setFixedSize(btnRedoToolbar, undoRedoBtnSize);
    btnUndoToolbar.setToolTipText("Redo");
    undoRedoPanel.add(btnRedoToolbar);

    JToggleButton btnPencil = new JToggleButton("Pencil");
    btnPencil.setAlignmentX(Component.LEFT_ALIGNMENT);
    Utils.setFixedSize(btnPencil, paintingToolBtnSize);
    btnPencil.setSelected(true);
    btnPencil.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent arg0) {
        canvas.getTool().setTool(DrawingTools.PENCIL);
      }
    });
    toolbar.add(btnPencil);

    JToggleButton btnBrush = new JToggleButton("Brush");
    Utils.setFixedSize(btnBrush, paintingToolBtnSize);
    btnBrush.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent arg0) {
        canvas.getTool().setTool(DrawingTools.BRUSH);
      }
    });
    toolbar.add(btnBrush);

    JToggleButton btnEraser = new JToggleButton("Eraser");
    Utils.setFixedSize(btnEraser, paintingToolBtnSize);
    btnEraser.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent arg0) {
        canvas.getTool().setTool(DrawingTools.ERASER);
      }
    });
    toolbar.add(btnEraser);

    JToggleButton btnLine = new JToggleButton("Line");
    Utils.setFixedSize(btnLine, paintingToolBtnSize);
    btnLine.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent arg0) {
        canvas.getTool().setTool(DrawingTools.LINE);
      }
    });
    toolbar.add(btnLine);

    JToggleButton btnRect = new JToggleButton("Rect");
    Utils.setFixedSize(btnRect, paintingToolBtnSize);
    btnRect.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent arg0) {
        canvas.getTool().setTool(DrawingTools.RECT);
      }
    });
    toolbar.add(btnRect);

    JToggleButton btnFilledRect = new JToggleButton("Filled Rect");
    Utils.setFixedSize(btnFilledRect, paintingToolBtnSize);
    btnFilledRect.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent arg0) {
        canvas.getTool().setTool(DrawingTools.FILL_RECT);
      }
    });
    toolbar.add(btnFilledRect);

    JToggleButton btnEllipse = new JToggleButton("Ellipse");
    Utils.setFixedSize(btnEllipse, paintingToolBtnSize);
    btnEllipse.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent arg0) {
        canvas.getTool().setTool(DrawingTools.ELLIPSE);
      }
    });
    toolbar.add(btnEllipse);

    JToggleButton btnFilledEllipse = new JToggleButton("Filled Ellipse");
    Utils.setFixedSize(btnFilledEllipse, paintingToolBtnSize);
    btnFilledEllipse.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent arg0) {
        canvas.getTool().setTool(DrawingTools.FILL_ELLIPSE);
      }
    });
    toolbar.add(btnFilledEllipse);

    JToggleButton btnFill = new JToggleButton("Fill");
    Utils.setFixedSize(btnFill, paintingToolBtnSize);
    btnFill.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent arg0) {
        canvas.getTool().setTool(DrawingTools.FILL);
      }
    });
    toolbar.add(btnFill);

    ButtonGroup buttonGroup = new ButtonGroup();
    buttonGroup.add(btnPencil);
    buttonGroup.add(btnBrush);
    buttonGroup.add(btnEraser);
    buttonGroup.add(btnLine);
    buttonGroup.add(btnRect);
    buttonGroup.add(btnEllipse);
    buttonGroup.add(btnFilledRect);
    buttonGroup.add(btnFilledEllipse);
    buttonGroup.add(btnFill);
    buttonGroup.setSelected(btnPencil.getModel(), true);

    // the spinner is for setting brush size
    JSpinner spinner = new JSpinner(new SpinnerNumberModel(Utils.INITIAL_STROKE_SIZE, 1, 100, 1));
    spinner.setToolTipText("Brush size");
    // Listen for changes to inform canvas about new Strokes
    spinner.addChangeListener(new ChangeListener() {
      @Override
      public void stateChanged(ChangeEvent e) {
        Integer value = (Integer) ((JSpinner) e.getSource()).getValue();
        BasicStroke stroke = canvas.getTool().getStroke();
        canvas.getTool().setStroke(new BasicStroke(value, stroke.getEndCap(), stroke.getLineJoin()));
      }
    });
    spinner.addKeyListener(new KeyAdapter() {
      @Override
      public void keyReleased(KeyEvent e) {
        if (e.getKeyCode() == KeyEvent.VK_ENTER) {
          try {
            ((JSpinner) e.getSource()).commitEdit();
          } catch (ParseException e1) {
          }
        }
        super.keyReleased(e);
      }
    });
    Utils.setFixedSize(spinner, paintingToolBtnSize);
    toolbar.add(spinner);

    JButton btnColour = buildCustomColorPickerButton();
    Utils.setFixedSize(btnColour, paintingToolBtnSize);
    toolbar.add(btnColour);

    // JToolbar has fixed BoxLayout. accept it. work with it.
    btnBrush.setAlignmentX(Component.CENTER_ALIGNMENT);
    btnColour.setAlignmentX(Component.CENTER_ALIGNMENT);
    btnEllipse.setAlignmentX(Component.CENTER_ALIGNMENT);
    btnEraser.setAlignmentX(Component.CENTER_ALIGNMENT);
    btnFill.setAlignmentX(Component.CENTER_ALIGNMENT);
    btnFilledEllipse.setAlignmentX(Component.CENTER_ALIGNMENT);
    btnFilledRect.setAlignmentX(Component.CENTER_ALIGNMENT);
    btnLine.setAlignmentX(Component.CENTER_ALIGNMENT);
    btnPencil.setAlignmentX(Component.CENTER_ALIGNMENT);
    btnRect.setAlignmentX(Component.CENTER_ALIGNMENT);
    spinner.setAlignmentX(Component.CENTER_ALIGNMENT);
    undoRedoPanel.setAlignmentX(Component.CENTER_ALIGNMENT);
  }

  /**
   * Create a button which displays a customized version of JColorChooser
   */
  private JButton buildCustomColorPickerButton() {
    JButton btnColour = new JButtonAsColor();
    btnColour.addActionListener(new ActionListener() {
      JColorChooser jc = new JColorChooser();
      {
        jc.setPreviewPanel(new JPanel());
      }

      @Override
      public void actionPerformed(final ActionEvent e) {
        final DrawingToolInfo tool = canvas.getTool();
        jc.setColor(tool.getColor() == null ? Color.BLACK : tool.getColor());

        JColorChooser.createDialog( //
            frame, // parent 
            "Pick color", // title
            true, // modal
            jc, // jcolorchooser
            new ActionListener() { // OK action listener
              @Override
              public void actionPerformed(ActionEvent ee) {
                Color color = jc.getColor();
                if (color != null) {
                  tool.setColor(color); // update tool color
                  ((JButtonAsColor) e.getSource()).setColor(color); // update button view
                }
              }
            }, //
            null // cancel action listener
            ).setVisible(true);
      }
    });

    return btnColour;
  }

  /**
   * Update GUI after undo/redo events. Disable/enable appropriate buttons and menu items.
   */
  @Override
  public void update() {
    btnRedo.setText(manager.getRedoPresentationName());
    btnUndo.setText(manager.getUndoPresentationName());
    btnRedo.setEnabled(manager.canRedo());
    btnUndo.setEnabled(manager.canUndo());

    btnRedoToolbar.setToolTipText(manager.getRedoPresentationName());
    btnUndoToolbar.setToolTipText(manager.getUndoPresentationName());
    btnRedoToolbar.setEnabled(manager.canRedo());
    btnUndoToolbar.setEnabled(manager.canUndo());
  }

  ///////////////////// ACTIONS /////////////////////
  private void newImage() {
    if (canvas.wasEdited()) {
      int rtrn = JOptionPane.showConfirmDialog(frame, "Do You wish to save changes first?");
      if (rtrn == JOptionPane.YES_OPTION) {
        save();
        doNew();
      } else if (rtrn == JOptionPane.CANCEL_OPTION) {
        return;
      } else {
        doNew();
      }
    } else {
      doNew();
    }
  }

  private void doNew() {
    ResizeDialog dialog = new ResizeDialog(canvas.getImage().getWidth(), canvas.getImage().getHeight(), "New Image");
    Point newSize = dialog.getNewSize();
    if (newSize != null) {
      saveFile = null;
      canvas.setImage(Utils.defaultImage(newSize.x, newSize.y));
      manager.discardAllEdits();
    }
  }

  private void resize() {
    BufferedImage old = canvas.getImage();
    ResizeDialog dialog = new ResizeDialog(old.getWidth(), old.getHeight(), "Resize Image");
    Point newSize = dialog.getNewSize();
    if (newSize != null) {
      if (!newSize.equals(new Point(old.getWidth(), old.getHeight()))) {
        BufferedImage nu = Utils.resizeImage(old, newSize);
        canvas.getResizeEditManager().editPerformed(new ResizeCmd(old, nu));
      }
    }
  }

  private void open() {
    if (canvas.wasEdited()) {
      int rtrn = JOptionPane.showConfirmDialog(frame, "Do You wish to save changes first?");
      if (rtrn == JOptionPane.YES_OPTION) {
        save();
        doOpen();
      } else if (rtrn == JOptionPane.CANCEL_OPTION) {
        return;
      } else {
        doOpen();
      }
    } else {
      doOpen();
    }
  }

  private void doOpen() {
    JFileChooser jfc = new JFileChooser();
    jfc.setMultiSelectionEnabled(false);
    jfc.setFileFilter(Utils.supportedFileFilter);

    if (jfc.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION) {
      File file = jfc.getSelectedFile();
      BufferedImage loadedImage = Utils.loadImage(file);
      if (loadedImage != null) {
        saveFile = null;
        canvas.setImage(loadedImage);
      }
    }
  }

  private void save() {
    if (saveFile == null) {
      saveAs();
    } else {
      Utils.writeImage(canvas.getImage(), saveFile);
    }
  }

  private void saveAs() {
    JFileChooser jfc = new JFileChooser();
    jfc.setFileFilter(Utils.supportedFileFilter);
    if (jfc.showSaveDialog(frame) == JFileChooser.APPROVE_OPTION) {
      File file = jfc.getSelectedFile();
      if (file != null) {
        saveFile = file;
        Utils.writeImage(canvas.getImage(), file);
      }
    }
  }

  private void exit() {
    if (canvas.wasEdited()) {
      int rtrn = JOptionPane.showConfirmDialog(frame, "Do You wish to save changes first?");
      if (rtrn == JOptionPane.YES_OPTION) {
        save();
      } else if (rtrn == JOptionPane.CANCEL_OPTION) {
        return;
      } else {
        doExit();
      }
    } else {
      doExit();
    }
  }

  private void doExit() {
    System.exit(0); // :)
  }
}
